<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>16.2.1-递归下降算法构造模板 AST</title>
</head>
<body>
    <div>代码示例作为参考，此页面代码不能执行</div>
</body>
    <script>
        /**
         * 实现一个更加完善的模板解析器
         * 
         * parseChildren 函数是整个解析器的核心
         * - 会递归地调用它来不断地消费模板内容
         * - parseChildren 函数本质上也是一个状态机，该状态机有多少种状态取决于子节点的类型数量
         * 
         * 在模板中，元素的子节点可以是以下几种
         * - 标签节点，例如 <div>。
         * - 文本插值节点，例如 {{ val }}。
         * - 普通文本节点，例如：text。
         * - 注释节点，例如 <!----\>。 （ 这 \ 是防止报错）
         * - CDATA 节点，例如 <![CDATA[ xxx ]]>
         * 
         * 状态迁移过程
         * - 当遇到字符 < 时，进入临时状态
         *   - 如果下一个字符匹配正则 /a-z/i，则认为这是一个标签节点，于是调用 parseElement 函数完成标签的解析
         *   - 如果字符串以 <!-- 开头，则认为这是一个注释节点，于是调用 parseComment 函数完成注释节点的解析
         *   - 如果字符串以 <![CDATA[ 开头，则认为这是一个 CDATA 节点，于是调用 parseCDATA 函数完成 CDATA 节点的解析
         * - 如果字符串以 {{ 开头，则认为这是一个插值节点，于是调用 parseInterpolation 函数完成插值节点的解析
         * - 其他情况，都作为普通文本，调用 parseText 函数完成文本节点的解析。
         * 
         * 几点需要注意
         * - parseChildren 函数的返回值是由子节点组成的数组，每次 while 循环都会解析一个或多个节点，
         *    这些节点会被添加到 nodes 数组中，并作为parseChildren 函数的返回值返回
         * - 只有处于 DATA 模式或 RCDATA 模式时，解析器才支持插值节点的解析。
         *    并且，只有处于DATA 模式时，解析器才支持标签节点、注释节点和 CDATA 节点的解析
         * - 当遇到特定标签时，解析器会切换模式。
         *    一旦解析器切换到 DATA 模式和 RCDATA 模式之外的模式时，一切字符都将作为文本节点被解析。
         *    即使在 DATA 模式或 RCDATA 模式下，如果无法匹配标签节点、注释节点、CDATA 节点、插值节点，那么也会作为文本节点解析
         * 
         * parseChildren 解析函数是整个状态机的核心，状态迁移操作都在该函数内完成
         * - 调用 parseElement 解析函数，这会间接地调用 parseChildren 函数，并产生一个新的状态机
         * - 标签嵌套层次的增加，新的状态机会随着 parseChildren 函数被递归地调用而不断创建，这就是 “递归下降” 中 “递归” 二字的含义
         * - 上级 parseChildren 函数的调用用于构造上级模板 AST 节点，
         *    被递归调用的下级 parseChildren 函数则用于构造下级模板 AST 节点。
         *    最终，会构造出一棵树型结构的模板 AST
        */


        // 定义文本模式，作为一个状态表
        const TextModes = {
            DATA: 'DATA', // 普通文本模式
            RCDATA: 'RCDATA', // 受限的 CDATA 模式
            RAWTEXT: 'RAWTEXT', // 原始文本模式
            CDATA: 'CDATA' // CDATA 模式
        }

        // 解析器函数，接收模板作为参数
        function parse(str) {
            // 定义上下文对象
            const context = {
                // source 是模板内容，用于在解析过程中进行消费
                source: str,
                // 解析器当前处于文本模式，初始模式为 DATA
                mode: TextModes.DATA
            }
            // 调用 parseChildren 函数开始进行解析，它返回解析后得到的子节点
            // parseChildren 函数接收两个参数：
            // 第一个参数是上下文对象 context
            // 第二个参数是由父代节点构成的节点栈，初始时栈为空
            const nodes = parseChildren(context, [])

            // 解析器返回 Root 根节点
            return {
                type: 'Root',
                // 使用 nodes 作为根节点的 children
                children: nodes
            }
        }

        function parseChildren(context, ancestors){
            // 定义 nodes 数组存储子节点，它将作为最终的返回值
            let nodes = []
            // 从上下文对象中取得当前状态，包括模式 mode 和模板内容 source
            const { mode, source } = context

            // 开启 while 循环，只要满足条件就会一直对字符串进行解析
            // 关于 isEnd() 后文会详细讲解
            while(!isEnd(context, ancestors)) {
                let node
                // 只有 DATA 模式和 RCDATA 模式才支持插值节点的解析
                if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
                    // 只有 DATA 模式才支持标签节点的解析
                    if (mode === TextModes.DATA && source[0] === '<') {
                        if (source[1] === '!') {
                            if (source.startsWith('<!--')) {
                                // 注释
                                node = parseComment(context)
                            } else if (source.startsWith('<![CDATA[')) {
                                // CDATA
                                node = parseCDATA(context, ancestors)
                            }
                        } else if (source[1] === '/') {
                            // 结束标签，这里需要抛出错误，后文会详细解释原因
                        } else if (/[a-z]/i.test(source[1])) {
                            // 标签
                            node = parseElement(context, ancestors)
                        }
                    } else if (source.startsWith('{{')) {
                        // 解析插值
                        node = parseInterpolation(context)
                    }
                }

                // node 不存在，说明处于其他模式，即非 DATA 模式且非 RCDATA 模式
                // 这时一切内容都作为文本处理
                if (!node) {
                    // 解析文本节点
                    node = parseText(context)
                }

                // 将节点添加到 nodes 数组中
                nodes.push(node)
            }

            // 当 while 循环停止后，说明子节点解析完毕，返回子节点
            return nodes
        }

        function parseElement(){
            // 解析开始标签
            const element = parseTag()
            // 这里递归地调用 parseChildren 函数进行 <div> 标签子节点的解析
            element.children = parseChildren()
            // 解析结束标签
            parseEndTag()

            return element
        }


    </script>
    

</html>